본문으로 건너뛰기

useReducer

🧑🏻‍💻 useReducer


useReducer 는 컴포넌트에 reducer를 추가할 수 있는 React Hook이다.

✅ useReducer 문법

const [state, dispatch] = useReducer(reducer, initialArg, init?)
import { useReducer } from 'react';

// state 업데이트 로직을 컴포넌트 외부의 reducer 함수로 분리한 경우
function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
  • useReduceruseState와 유사한 방식으로 현재 state를 제공하고, 업데이트하는 dispatch 함수를 반환한다.

  • reducer는 state가 업데이트되는 방식을 지정하는 함수이다.

  • initialArg는 초기 state가 계산되는 값이다.

  • init은 초기 state 계산 방법을 지정하는 초기화 함수이다.

    • 이것을 지정하지 않으면 초기 state는 initialArg로 설정되고 초기 state는 init(initialArg)를 호출한 결과로 설정된다.

🧑🏻‍💻 알고 가기


✅ dispatch 함수

  • state를 다른 값으로 업데이트하고 리렌더링한다.
  • 매개변수로 action을 전달받고 반환값은 없다.
    • action은 사용자가 수행한 작업이다. 보통 type 속성이 있는 객체가 온다.

      const [state, dispatch] = useReducer(reducer, { age: 42 });

      function handleClick() {
      dispatch({ type: 'incremented_age' });
      // ...

✅ reducer 함수

  • state를 어떻게 변경할지 action 로직을 작성하는 함수다. 보통 type을 활용한 if 문과 switch case 문으로 로직을 작성한다.

  • 이때 state는 읽기 전용으로, 배열과 객체 state의 경우 변이하지 않고 교체해야 한다. 이를 신경 쓰기 싫다면 useImmerReducer 를 사용하자.

🧑🏻‍💻 활용 및 생각할 거리


✅ 초기 state 재생성 방지

  • React는 초기 state를 한 번 저장하고 다음 렌더링에서 이를 무시한다.

  • 함수를 활용하여 initialArg를 설정하고 싶다면 불필요한 함수 호출 방지를 위해 세번 째 인수에 초기화 함수를 전달하자.

    function createInitialState(username) {
    // ...
    }

    function TodoList({ username }) {
    const [state, dispatch] = useReducer(reducer, username, createInitialState);
    // ...

✅ useState vs. useReducer

  1. 코드 크기
  • useState를 사용하면 미리 작성해야 하는 코드가 줄어든다.

  • useReducer를 사용하면 reducer 함수와 action을 전달하는 부분 모두 작성해야 한다.
    → 하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우 useReducer가 코드를 줄이는 데 도움이 될 수 있다.

  1. 가독성
  • 간단한 state 업데이트에는 useState 가 좋다.

  • 구조가 복잡한 state 업데이트에는 useReducer 가 좋다.
    useReducer를 사용하면 업데이트 로직이 어떻게 동작 하는지와 이벤트 핸들러를 통해 무엇이 일어났는지를 깔끔하게 분리할 수 있다.

  1. 디버깅
  • useState는 디버깅이 어렵다.

  • useReducer는 reducer에 콘솔 로그를 추가하여 모든 state 업데이트에 대한 버그를 한 눈에 확인할 수 있다. → 각 action이 정확하다면, 버그가 reducer 로직 자체에 있다는 것을 쉽게 판단할 수 있다.

  1. 테스팅
  • useReduceruseState와 매우 유사하지만 컴포넌트에 의존하지 않는 순수한 함수이기 때문에, 이벤트 핸들러의 state 업데이트 로직을 컴포넌트 외부의 단일 함수로 옮길 수 있고 이를 테스트에도 사용할 수 있다.

✅ useState 대신 useReducer을 사용하면 좋은 경우

  • 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우 useReducer을 사용하면 코드를 줄일 수 있다.

    import React, { useState } from 'react';

    const Counter = () => {
    const [count, setCount] = useState(0);

    const increment = () => {
    setCount(count + 1);
    };

    const decrement = () => {
    setCount(count - 1);
    };

    const reset = () => {
    setCount(0);
    };

    return (
    <div>
    Count: {count}
    <button onClick={increment}>증가</button>
    <button onClick={decrement}>감소</button>
    <button onClick={reset}>리셋</button>
    </div>
    );
    };

    export default Counter;
  • 다음 코드에서 하나의 state에 대해서 많은 eventHandler 함수들을 만들어 내야 한다. 하지만 reducer을 사용할 경우 이러한 부분들을 더 명료하게 작성할 수 있다.

    import React, { useReducer } from 'react';

    // 초기 상태 설정
    const initialState = {
    count: 0,
    };

    // 리듀서 함수
    const reducer = (state, action) => {
    switch (action.type) {
    case 'increment':
    return { ...state, count: state.count + 1 };
    case 'decrement':
    return { ...state, count: state.count - 1 };
    case 'reset':
    return initialState;
    default:
    throw new Error();
    }
    };

    const Counter = () => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
    <div>
    Count: {state.count}
    <button onClick={() => dispatch({ type: 'increment' })}>증가</button>
    <button onClick={() => dispatch({ type: 'decrement' })}>감소</button>
    <button onClick={() => dispatch({ type: 'reset' })}>리셋</button>
    </div>
    );
    };

    export default Counter;
  • 이처럼 reducer 함수를 정의하고 dispatch 함수를 통해 가독성을 높이고 개발자 의도가 잘 드러나는 코드를 작성할 수 있다.

  • 이때 {…state, …}에서 …state를 spread operator로 펼쳐주는 이유는 state는 불변성을 유지하기 위함이다. 객체를 복사해서 참조 값으로 넣어주기 때문에 원본 객체를 손상시키지 않고 참조 값을 변경해 줄 수 있다.